<body>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <h2>Client-side video transcoding with FFmpeg.wasm</h2>

  <video width="400" controls id="videoPlayer" muted="muted">Your browser does not support HTML video.</video>

  <p>See the <a href="https://blog.scottlogic.com/2020/11/23/ffmpeg-webassembly.html">associated blog post</a>.</p>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />

  <script src="/asset/mux.min.js"></script>
  <script src="/asset/ffmpeg.min.js"></script>
  <script src="/asset/rxjs.umd.js"></script>
  <script>
    const { Observable, fromEvent, partition, combineLatest, zip } = rxjs;
    const { map, flatMap, take, skip } = rxjs.operators;

    const bufferStream = filename =>
      new Observable(async subscriber => {
        const ffmpeg = FFmpeg.createFFmpeg({
          corePath: '/asset/ffmpeg-core.js',
          log: true
        });

        const fileExists = file => ffmpeg.FS('readdir', '/').includes(file);
        const readFile = file => ffmpeg.FS('readFile', file);

        await ffmpeg.load();
        const sourceBuffer = await fetch(filename).then(r => r.arrayBuffer());
        ffmpeg.FS('writeFile', 'input.mp4', new Uint8Array(sourceBuffer, 0, sourceBuffer.byteLength));

        let index = 0;

        ffmpeg
          .run(
            '-i',
            'input.mp4',
            '-g',
            '1',
            // Encode for MediaStream
            '-segment_format_options',
            'movflags=frag_keyframe+empty_moov+default_base_moof',
            // encode 5 second segments
            '-segment_time',
            '5',
            // write to files by index
            '-f',
            'segment',
            '%d.mp4'
          )
          .then(() => {
            // send out the remaining files
            while (fileExists(`${index}.mp4`)) {
              subscriber.next(readFile(`${index}.mp4`));
              index++;
            }
            subscriber.complete();
          });

        setInterval(() => {
          // periodically check for files that have been written
          if (fileExists(`${index + 1}.mp4`)) {
            subscriber.next(readFile(`${index}.mp4`));
            index++;
          }
        }, 200);
      });

    const mediaSource = new MediaSource();
    const videoPlayer = document.getElementById('videoPlayer');
    videoPlayer.src = URL.createObjectURL(mediaSource);
    videoPlayer.play();

    const mediaSourceOpen = fromEvent(mediaSource, 'sourceopen');

    const bufferStreamReady = combineLatest(mediaSourceOpen, bufferStream('/asset/test.wmv')).pipe(map(([, a]) => a));

    const sourceBufferUpdateEnd = bufferStreamReady.pipe(
      take(1),
      map(buffer => {
        // create a buffer using the correct mime type
        const mime = `video/mp4; codecs="${muxjs.mp4.probe
          .tracks(buffer)
          .map(t => t.codec)
          .join(',')}"`;
        const sourceBuf = mediaSource.addSourceBuffer(mime);

        // append the buffer
        mediaSource.duration = 5;
        sourceBuf.timestampOffset = 0;
        sourceBuf.appendBuffer(buffer);

        // create a new event stream
        return fromEvent(sourceBuf, 'updateend').pipe(map(() => sourceBuf));
      }),
      flatMap(value => value)
    );

    zip(sourceBufferUpdateEnd, bufferStreamReady.pipe(skip(1)))
      .pipe(
        map(([sourceBuf, buffer]) => {
          mediaSource.duration += 5;
          sourceBuf.timestampOffset += 5;
          sourceBuf.appendBuffer(buffer.buffer);
        })
      )
      .subscribe();
  </script>
</body>
